"""
钉钉认证工具类
"""
import httpx
import json
import logging
from typing import Optional, Dict
from urllib.parse import urlencode
from utils.config import config


logger = logging.getLogger(__name__)


class OAuthError(Exception):
    """第三方登录异常"""
    pass


class DingTalkUtils:
    """钉钉认证工具类 - 支持OAuth 2.0扫码登录"""
    
    # 钉钉API地址
    DINGTALK_OAUTH_BASE = "https://api.dingtalk.com/v1.0"
    
    @staticmethod
    def _extract_error(response: httpx.Response) -> str:
        """提取错误信息"""
        try:
            data = response.json()
            return json.dumps(data, ensure_ascii=False)
        except (ValueError, json.JSONDecodeError):
            text = response.text.strip()
            return text or "未知错误"
    
    @staticmethod
    async def get_user_access_token(auth_code: str) -> Optional[Dict]:
        """
        通过OAuth授权码获取用户access_token
        :param auth_code: OAuth授权码
        :return: token信息 {"accessToken": "xxx", "refreshToken": "xxx", "expireIn": 7200}
        """
        url = f"{DingTalkUtils.DINGTALK_OAUTH_BASE}/oauth2/userAccessToken"
        headers = {
            "Content-Type": "application/json"
        }
        data = {
            "clientId": config.DINGTALK_CLIENT_ID,
            "clientSecret": config.DINGTALK_CLIENT_SECRET,
            "code": auth_code,
            "grantType": "authorization_code"
        }
        
        async with httpx.AsyncClient() as client:
            try:
                response = await client.post(url, headers=headers, json=data, timeout=30.0)
                
                if response.status_code >= 400:
                    error_detail = DingTalkUtils._extract_error(response)
                    logger.warning(
                        "DingTalk access token request failed: status=%s detail=%s",
                        response.status_code,
                        error_detail,
                    )
                    raise OAuthError(
                        f"获取钉钉 access token 失败（HTTP {response.status_code}）：{error_detail}"
                    )
                
                result = response.json()
                access_token = result.get("accessToken")
                
                if not access_token:
                    detail = json.dumps(result, ensure_ascii=False)
                    logger.error(
                        "DingTalk access token response missing fields: %s", detail
                    )
                    message = result.get("message") or result.get("errmsg")
                    if message:
                        raise OAuthError(f"钉钉返回 access token 数据异常：{message}")
                    raise OAuthError("钉钉返回数据缺失 accessToken")
                
                return result
            except OAuthError:
                raise
            except Exception as e:
                logger.error(f"请求钉钉API异常: {e}")
                import traceback
                traceback.print_exc()
                raise OAuthError(f"请求钉钉API异常: {str(e)}")
    
    @staticmethod
    async def get_user_info_by_code(auth_code: str) -> Optional[Dict]:
        """
        通过OAuth授权码获取用户信息
        :param auth_code: OAuth授权码
        :return: 用户信息
        """
        # 第1步: 获取用户access_token
        token_info = await DingTalkUtils.get_user_access_token(auth_code)
        if not token_info or "accessToken" not in token_info:
            raise OAuthError("获取用户access_token失败")
        
        user_access_token = token_info["accessToken"]
        open_id = token_info.get("openId")
        
        # 第2步: 使用用户access_token获取用户信息
        url = f"{DingTalkUtils.DINGTALK_OAUTH_BASE}/contact/users/me"
        headers = {
            "x-acs-dingtalk-access-token": user_access_token
        }
        
        async with httpx.AsyncClient() as client:
            try:
                response = await client.get(url, headers=headers, timeout=30.0)
                
                if response.status_code >= 400:
                    error_detail = DingTalkUtils._extract_error(response)
                    logger.error(
                        "DingTalk contact users/me request failed: status=%s detail=%s",
                        response.status_code,
                        error_detail,
                    )
                    raise OAuthError(
                        f"获取钉钉用户信息失败（HTTP {response.status_code}）：{error_detail}"
                    )
                
                result = response.json()
                
                # 提取uid（openId或unionId）
                uid = open_id or result.get("openId") or result.get("unionId")
                if not uid:
                    detail = json.dumps(result, ensure_ascii=False)
                    logger.error(
                        "DingTalk contact users/me response missing openId/unionId: %s",
                        detail,
                    )
                    raise OAuthError("钉钉返回数据缺失 openId/unionId")
                
                # 返回用户信息，添加userid字段用于兼容
                user_info = result
                # 钉钉OAuth返回的字段可能不同，需要映射
                if "unionId" in user_info:
                    user_info["unionid"] = user_info["unionId"]
                user_info["openId"] = uid  # 确保有openId
                user_info["userid"] = uid  # 使用openId作为userid
                
                return user_info
            except OAuthError:
                raise
            except Exception as e:
                logger.error(f"请求钉钉API异常: {e}")
                import traceback
                traceback.print_exc()
                raise OAuthError(f"请求钉钉API异常: {str(e)}")
    
    @staticmethod
    async def get_user_detail(userid: str) -> Optional[Dict]:
        """
        获取用户详细信息（OAuth 2.0流程已经在get_user_info_by_code中获取）
        :param userid: 用户ID
        :return: 用户详细信息
        """
        # OAuth 2.0流程中已经获取了详细信息，这里直接返回None
        # 如果需要更多信息，可以在get_user_info_by_code中一次性获取
        return None
    
    @staticmethod
    def generate_login_url(redirect_uri: str = None) -> str:
        """
        生成钉钉登录URL
        :param redirect_uri: 回调地址
        :return: 登录URL
        """
        if not redirect_uri:
            redirect_uri = config.DINGTALK_REDIRECT_URI
        
        # 使用urlencode确保URL编码正确
        params = {
            "redirect_uri": redirect_uri,
            "response_type": "code",
            "client_id": config.DINGTALK_CLIENT_ID,
            "scope": "openid",
            "prompt": "consent",
        }
        
        return "https://login.dingtalk.com/oauth2/auth?" + urlencode(params)

